// Copyright (c) 2007, Brian Duff

// See LICENSE for full license details.

package org.dubh.jdant;

import java.io.File;
import java.io.IOException;

import java.net.URL;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;


/**
 * Represents a view on a JDeveloper project file.
 *
 * @author brian.duff@dubh.org
 */
public final class ProjectFile
{
  private static final String DEPENDENCY_CONFIG = "oracle.ide.model.DependencyConfiguration";
  private static final String COMPILER_CONFIG = "oracle.jdeveloper.compiler.OjcConfiguration";
  private static final String DEFAULT_COPY_TYPES = ".gif;.GIF;.jpg;.JPG;.jpeg;.JPEG;.png;.PNG;.properties;.xml;-apf.xml;.ejx;.xcfg;.cpx;.dcx;.sva;.wsdl;.ini;.tld;.tag;.xlf"; 
  
  private static final String LIB_CONFIG = "oracle.jdevimpl.config.JProjectLibraries";
  
  private final File _projectFile;
  private final File _oracleHome;
  private final ROHashStructure _hash;
  
  private Map<String,Library> _oracleHomeLibraries;
  
  /**
   * Creates an instance of <tt>ProjectFile</tt for the specified 
   * project. This constructor should be used only for projects that do not
   * use named (extension) libraries.
   * 
   * @param projectFile the project file.
   * @throws IOException if the project file could not be loaded.
   */
  public ProjectFile( File projectFile ) throws IOException
  {
    this( projectFile, null );
  }
  
  /**
   * Creates an instance of <tt>ProjectFile</tt for the specified 
   * project.<p>
   * 
   * This constructor takes a <tt>oracleHome</tt> option, which corresponds
   * to the install directory of JDeveloper. This is used to locate
   * extension (named) libraries.
   * 
   * @param projectFile a project file.
   * @param oracleHome the JDeveloper installation.
   * @throws IOException if an error occurs.
   */
  public ProjectFile( File projectFile, File oracleHome ) throws IOException
  {
    _projectFile = projectFile;
    _hash = load( projectFile );
    _oracleHome = oracleHome;
  }
  
  public ROHashStructure hashStructure()
  {
    return _hash;
  }
  
  public File file()
  {
    return _projectFile;
  }
  
  /**
   * Returns the name of the project. For example, a project called 
   * "foo.jpr" would return "foo" from this method.
   * 
   * @return the name of the project.
   */
  public String name()
  {
    String fileName = _projectFile.getName();
    int dot = fileName.lastIndexOf( '.' );
    if ( dot == -1 ) return fileName;
    return fileName.substring( 0, dot );
  }
  
  /**
   * Returns the default package of the project.
   * 
   * @return the default package of the project.
   */
  public String defaultPackage()
  {
    return _hash.getString( "defaultPackage" );
  }
  
  /**
   * Returns the output directory of the project.
   * 
   * @return the output directory of the project.
   */
  public File outputDirectory()
  {
    URL outputDirectory = _hash.getURL( "oracle.jdevimpl.config.JProjectPaths/outputDirectory" );
    return Util.normalize( new File( outputDirectory.getPath() ) );
  }
  
  /**
   * Returns the compiler options to be used when OJC is selected as the 
   * compiler.
   * 
   * @return the options to pass to ojc.
   */
  public String ojcOptions()
  {
    return _hash.getString( COMPILER_CONFIG + "/Ojc.commandline.options" );
  }
  
  /**
   * Returns the compiler options to be used when Javac is selected as the
   * compiler.
   * 
   * @return the options to pass to javac.
   */
  public String javacOptions()
  {
    return _hash.getString( COMPILER_CONFIG +"/Javac.commandline.options" );
  }
  
  /**
   * Returns the name of the compiler to use. Either "Javac" or "Ojc".
   * 
   * @return the name of the compiler to use. Either "Javac" or "Ojc".
   */
  public String compilerName()
  {
    return _hash.getString( COMPILER_CONFIG + "/compiler.name", "Ojc" );
  }
  
  /**
   * Get the file types to include when copying from the source directory
   * to the output directory, expressed using ant filter syntax.
   * 
   * @return the copy includes for this project.
   */
  public String copyIncludes()
  {
    ROHashStructure config = _hash.getHashStructure( COMPILER_CONFIG );
    List<Object> res = config.getListStructure( "copyRes" );
    if ( res == null )
    {
      return suffixesToAntIncludes( DEFAULT_COPY_TYPES );
    }
    else
    {
      return listToAntIncludes( res );
    }
  }
  
  /**
   * Gets projects that this project depends on.
   * 
   * @return a list of File instances for projects that this project
   *    depends on. The collection is unmodifiable.
   */
  public List<File> dependencies()
  {
    ROHashStructure depConfig = _hash.getHashStructure( DEPENDENCY_CONFIG );
    if ( depConfig == null ) return Collections.emptyList();
    
    return Util.files( depConfig, "dependencyList", "sourceURL" );
  }
  
  /**
   * Gets libraries referenced by this project. This method will return
   * all 
   * @return
   * 
   * @throws InvalidProjectException if project libraries could not be
   *    resolved.
   */
  public List<Library> libraries()
  {    
    ROHashStructure libConfig = _hash.getHashStructure( LIB_CONFIG );
    if ( libConfig == null ) return Collections.emptyList();
    
    Collection<String> exportedIds = findExportedLibraryIds(libConfig);
    
    Map<String,Library> internalDefs = findInternalLibraries( libConfig,
       exportedIds );
    
    List<Library> libraries = new ArrayList<Library>();
    
    List<ROHashStructure> refs = Util.safeList( libConfig.getListOfHash( "libraryReferences" ) );
    for ( ROHashStructure ref : refs )
    {
      Library lib = loadLibrary( internalDefs, ref, exportedIds );
      if ( lib != null ) libraries.add( lib );
    }
    
    return Collections.unmodifiableList( libraries );
  }
  
  private Library loadLibrary( Map<String,Library> internalDefs, ROHashStructure ref, Collection<String> exportedIds )
  {
    Object id = ref.getObject( "id" );
    if ( id instanceof String )
      return loadNamedLibrary( internalDefs, (String) id, exportedIds );
    else if ( id instanceof URL )
      return loadExternalLibrary( (URL) id, exportedIds );
    else
      throw new IllegalStateException( file().getName() + ": Unknown library id type: " + id );
  }

  private Library loadExternalLibrary( URL library, Collection<String> exportedIds )
  {
    try
    {
      ExternalLibraryParser parser = new ExternalLibraryParser( Util.normalize( new File( library.getPath() ) ) );
      parser.setExportedIds( exportedIds );

      return parser.library( file() );
    }
    catch ( IOException e )
    {
      throw new InvalidProjectException( this, file().getName() + ": Error reading library " + library.getPath(), e );
    }
  }

  private Library loadNamedLibrary( Map<String,Library> internalDefs, String id, Collection<String> exportedIds )
  {
    Library lib = internalDefs.get( id );
    if ( lib != null ) return lib;
    
    if ( _oracleHomeLibraries == null )
    {
      if ( _oracleHome == null || !_oracleHome.isDirectory() )
      {
        throw new InvalidProjectException( this, file().getName() + ": No library called '" + id + "' is defined in the project." );
      }
      _oracleHomeLibraries = new LibraryScanner( _oracleHome ).findLibraries();
    }

    lib = _oracleHomeLibraries.get( id );
    if ( lib == null )
    {
      throw new InvalidProjectException( this, file().getName() + ": Library reference not found: " + id );
    }

    return lib;
  }

  private Map<String,Library> findInternalLibraries( ROHashStructure libConfig,
    Collection<String> exportedIds )
  {
    Map<String,Library> libs = new LinkedHashMap<String,Library>();
    
    ROHashStructure internalDefs = libConfig.getHashStructure( "internalDefinitions" );
    if ( internalDefs == null ) return libs;
    
    List<ROHashStructure> libraryDefs = Util.safeList( internalDefs.getListOfHash( "libraryDefinitions" ) );
    for ( ROHashStructure library : libraryDefs )
    {
      String id = library.getString( "id" );
      List<File> classpath = Util.toListOfFiles( library.getListOfURL( "classPath" ) );
      
      Library lib = new Library.Builder( id, classpath )
                    .exported( exportedIds.contains( id ) )
                    .type( LibraryType.PROJECT )
                    .build();
      libs.put( id, lib );
    }
    
    return libs;
  }

  private Collection<String> findExportedLibraryIds(ROHashStructure libConfig)
  {
    List<ROHashStructure> exported = 
      Util.safeList( libConfig.getListOfHash( "exportedReferences" ) );
    Collection<String> exportedIds = new HashSet<String>();
    for ( ROHashStructure hs : exported )
    {
      Object o = hs.getObject( "id" );
      if ( o instanceof String )
      {
        exportedIds.add( (String) o );
      }
      else if ( o instanceof URL )
      {
        // Have to convert a URL back into a relative path for a good
        // id lookup.
        File file = Util.normalize( new File( ((URL)o).getPath() ) );
        String relative = Util.toRelativePath( file(), file );
        exportedIds.add( relative );
      }
    }
    return exportedIds;
  }
  
  private static String suffixesToAntIncludes( String suffixes )
  {
    return listToAntIncludes( Arrays.asList( suffixes.split( ";" ) ) );
  }
  
  private static String listToAntIncludes( List suffixes )
  {
    StringBuilder antIncludes = new StringBuilder();
    for ( Object suffix : suffixes )
    {
      antIncludes.append( "**/*" );
      antIncludes.append( suffix );
      antIncludes.append( "," );
    }
    Strings.removeLastCharacter( antIncludes );
    
    return antIncludes.toString();
  }

  
  private ROHashStructure load( File file ) throws IOException
  {
    return new ROHashStructureIO().load( file );
  }
}
